# -*- coding: utf-8 -*-
"""
Created on Thu Jul 25 10:59:24 2024

@author: xiaobojie
"""

import sys, os, time, random, wave, argparse, pygame

import numpy as np

from collections import deque
import matplotlib.pyplot as plt


g_show_plot = False

# 音符的频率字典？
pm_notes = {'C4':262, 'Eb':311, 'F':349, 'G':391, 'Bb':466}

def write_wave(f_name, data):
    '''
    写入wav文件
    '''
    file = wave.open(f_name, 'wb')

    n_channel = 1
    
    # sample width决定了音频的动态范围和分辨率。位宽越大，音频文件能够表示的音频细节就越多，动态范围也越大，音质通常会更好。然而，较大的位宽也会增加文件的大小。
    # 例如，在一个16位的WAV文件中，每个样本占用2个字节（16位/8），而在一个24位的WAV文件中，每个样本占用3个字节（24位/8）。
    sample_wid = 2
    frame_rate = 44100
    
    # 举个例子，假设你有一个44.1kHz采样率的立体声WAV文件，每秒会有44100个frame，每个frame包含两个样本。这些样本的数据一起构成了音频信号。    
    n_frame = 44100

    file.setparams((n_channel, sample_wid, frame_rate, n_frame, 'NONE', 'noncompressed'))
    file.writeframes(data)
    file.close()



def gener_note(freq):
    '''
    生成音符
    '''
    n_samples = 44100
    
    # 一个44.1kHz的WAV文件意味着每秒钟采集44100个样本来表示音频信号。
    sample_rate = 44100
    n = int(sample_rate / freq)
    
    # deque 双端队列
    # 双向操作：可以在队列的两端执行插入和删除操作，既可以像栈（LIFO，后进先出）那样在一端操作，也可以像队列（FIFO，先进先出）那样在另一端操作。
    
    # random.random() - 0.5 for i in range(n)
    # 生成一个介于 0 到 1 之间的浮点数。
    # 将这个浮点数平移到 -0.5 到 0.5 的范围内。
    # 生成一个包含 n 个元素的列表，其中每个元素都是上述计算得到的随机数。
    buf = deque([random.random() - 0.5 for i in range(n)])
    if g_show_plot:
        
        # 在代码 axline, = ax.plot(buf) 中，逗号 , 是为了确保 ax.plot(buf) 返回的第一个对象（Line2D 对象）被分配给 axline 变量。
        # ax.plot 返回的是一个包含一个或多个 Line2D 对象的列表，即使只画了一条线，它返回的也是一个包含一个元素的列表。
        # 因此，需要使用逗号 , 来进行解包（unpack），将列表中的第一个元素直接赋值给 axline。
        # 列表解包：这种解包方式常用于返回多个值的函数调用中，确保每个值都可以被单独处理。
        # plt.plot(buf)可能返回多个对象，可能返回1个，所以ax, _ = plt.plot(buf)会报错
        ax,  = plt.plot(buf)

    # [0]*n_samples，每项是0，长度是n_samples的array
    samples = np.array([0]*n_samples, 'float32')
    
    # k算法
    for i in range(n_samples):
        samples[i] = buf[0]
        avg = .995*.5*(buf[0]+buf[1])
        buf.append(avg)
        
        # 弹出左边第一项，此处使用双端队列，运行速度快
        # 这种插入和删除过程是O(1)或“常数时间”的操作，这意味着不论deque的容器
        # 变得多大，它需要的时间都是相同的。
        buf.popleft()
        
        
        if g_show_plot:
            if i % 1000 == 0:
                ax.set_ydata(buf)
                plt.draw()
                
    # samples数组的每个值乘以32767（16位带符号整数的取值范围是—32768～32,767）
    samples = np.array(samples*32767, 'int16')
    
    # 转换成字符串表示形式，这是wave模块的要求
    # return samples.tostring()
    return samples.tobytes()
            

class Note_player:
    def __init__(self):
        #？
        pygame.mixer.pre_init(44100, -16, 1, 2048)
        pygame.init()
        # 音符字典
        self.notes = {}


    def add(self, f_name):
        self.notes[f_name] = pygame.mixer.Sound(f_name)




    def play(self, f_name):
        try:
            # self.notes[f_name]是什么对象？
            self.notes[f_name].play()
        except:
            print(f'{f_name}不存在！')



    def play_rand(self):
        index = random.randint(0, len(self.notes)-1)
        # 什么意思？
        note = list(self.notes.values())[index]
        note.play()
        



def main():
    global g_show_plot

    parser = argparse.ArgumentParser(description='使用Karplus算法生成音乐')

   
    parser.add_argument('--display', action='store_true', required=False)
    parser.add_argument('--play', action='store_true', required=False)
    parser.add_argument('--piano', action='store_true', required=False)
    args = parser.parse_args()

    # 输入参数display？
    if args.display:
        g_show_plot = True
        
        # plt.ion() 是 matplotlib 中用于启用交互式模式（interactive mode）的方法。
        # 当交互式模式开启时，图形在生成后会立即显示，并且会在每次修改时自动更新，而不需要调用 plt.show()。
        # 这对于需要频繁更新图形的场景非常有用，比如实时数据可视化、动态演示或交互式绘图。
        plt.ion()

    n_player = Note_player()


    print('正在生成音符……')
    for name, freq in list(pm_notes.items()):
        f_name = name + '.wav'
        if not os.path.exists(f_name) or args.display:
            data = gener_note(freq)
            print(f'正在生成{f_name}……')
            write_wave(f_name, data)
        else:
            print(f'{f_name}已存在，跳过……')


        n_player.add(name + '.wav')

        if args.display:
            n_player.play(name + '.wav')
            time.sleep(0.5)


    if args.play:
        while True:
            try:
                n_player.play_rand()
                rest = np.random.choice([1, 2, 4, 8], 1, p=[.15, .7, .1, .05])
                time.sleep(.25*rest[0])
            except KeyboardInterrupt:
                
                # 此处必须加sys
                sys.exit()


    if args.piano:
        while True:
            for event in pygame.event.get():
                if event.type == pygame.KEYUP:
                    print('按键按下')
                    n_player.play_rand()
                    time.sleep(0.5)
                        
if __name__ == '__main__':
    main()